#include "bga/graphics/Program.h"
#include "bga/graphics/Shader.h"
#include <iostream>

#define TO_STRING(s) (#s)
#define MATRIX_UNIFORM(dimension) (glUniformMatrix ## dimension ## fvARB(getLocation(location), 1, GL_FALSE, matrix))


using namespace std;

namespace bga
{

Program::Program() : GLObject()
{
    this->vs = NULL;
    this->fs = NULL;
    create();
}

Program::Program(Shader& vs, Shader& fs)
{
    this->vs = &vs;
    this->fs = &fs;
    create();
}

Program::~Program()
{
    this->destroy();
}

void Program::setShader(Shader& shader)
{
    switch(shader.getType())
    {
        case Shader::FRAGMENT_SHADER:
            this->fs = &shader;
            break;

        case Shader::VERTEX_SHADER:
            this->vs = &shader;
            break;

        default:
            break;
    }
    this->attachShader(shader);
}

void Program::setActive(bool active) const
{
    if(active)
        glUseProgram(this->id);
    else
        glUseProgram(0);
}

void Program::create()
{
    this->id = glCreateProgram();
    if(this->id == 0)
    {
        cout << "Program creation error" << endl;
    }
}

 void Program::link()
 {
    glLinkProgram(id);

    int res;
    glGetProgramiv(this->id, GL_LINK_STATUS, &res);

    if(res != GL_TRUE)
    {
        int sizeLog;
        glGetProgramiv(this->id, GL_INFO_LOG_LENGTH, &sizeLog);
        char* log = new char[sizeLog + 1];
        glGetProgramInfoLog(this->id, sizeLog, &sizeLog, log);
        cout << log << endl;
        delete[] log;
    }
 }

void Program::destroy()
{
    if(this->id != 0)
        glDeleteProgram(this->id);
}

void Program::attachShader(Shader& shader)
{
    switch(shader.getType())
    {
        case Shader::FRAGMENT_SHADER:
            if(this->fs != NULL)  /// That mean we already attach a fragment Shader
            {
                glDetachShader(this->id, shader.getId());
            }
            glAttachShader(this->id, shader.getId());
            break;

        case Shader::VERTEX_SHADER:
            if(this->vs != NULL)  /// That mean we already attach a fragment Shader
            {
                glDetachShader(this->id, shader.getId());
            }
            glAttachShader(this->id, shader.getId());
            break;

        default:
            break;
    }
}

int Program::getLocation(const std::string& location)
{
    if(this->uniformLocations.find(location) == this->uniformLocations.end())
    {
        this->uniformLocations[location] = glGetUniformLocationARB(this->getId(), location.c_str());
    }
    return this->uniformLocations[location];

}

void Program::setValue(const std::string& location, int value)
{
    glUniform1iARB(getLocation(location), value);
}

void Program::setValue(const std::string& location, float value)
{
    glUniform1fARB(getLocation(location), value);
}

void Program::setValues(const std::string& location, int value1, int value2)
{
    glUniform2iARB(getLocation(location), value1, value2);
}

void Program::setValues(const std::string& location, float value1, float value2)
{
    glUniform2fARB(getLocation(location), value1, value2);
}

void Program::setValues(const std::string& location, int value1, int value2, int value3)
{
    glUniform3iARB(getLocation(location), value1, value2, value3);
}

void Program::setValues(const std::string& location, float value1, float value2, float value3)
{
    glUniform3fARB(getLocation(location), value1, value2, value3);
}

void Program::setValues(const std::string& location, int value1, int value2, int value3, int value4)
{
    glUniform4iARB(getLocation(location), value1, value2, value3, value4);
}

void Program::setValues(const std::string& location, float value1, float value2, float value3, float value4)
{
    glUniform4fARB(getLocation(location), value1, value2, value3, value4);
}

void Program::setMatrix(const std::string& location, const float* matrix, unsigned int dimensions)
{
    if(dimensions > 0 && dimensions < 5)
    {
        /// Maybe try a preprocessor directive
        switch(dimensions)
        {
            case 2:
                glUniformMatrix2fvARB(getLocation(location), 1, GL_FALSE, matrix);
                break;

            case 3:
                glUniformMatrix3fvARB(getLocation(location), 1, GL_FALSE, matrix);
                break;

            case 4:
                glUniformMatrix4fvARB(getLocation(location), 1, GL_FALSE, matrix);
                break;
        }
    }
}

void Program::sendUniform(Uniform& uniform)
{
    switch (uniform.intList.size())
    {
        case 1:
            setValue(uniform.location, uniform.intList[0]);
            break;

        case 2:
            setValues(uniform.location, uniform.intList[0], uniform.intList[1]);
            break;

        case 3:
            setValues(uniform.location, uniform.intList[0], uniform.intList[1], uniform.intList[2]);
            break;

        case 4:
            setValues(uniform.location, uniform.intList[0], uniform.intList[1], uniform.intList[2], uniform.intList[3]);
            break;

        default:
            break;
    }

    switch (uniform.floatList.size())
    {
        case 1:
            setValue(uniform.location, uniform.floatList[0]);
            break;

        case 2:
            setValues(uniform.location, uniform.floatList[0], uniform.floatList[1]);
            break;

        case 3:
            setValues(uniform.location, uniform.floatList[0], uniform.floatList[1], uniform.floatList[2]);
            break;

        case 4:
            setValues(uniform.location, uniform.floatList[0], uniform.floatList[1], uniform.floatList[2], uniform.floatList[3]);
            break;

        default:
            break;
    }

    if (uniform.matrix != NULL)
    {
        setMatrix(uniform.location, uniform.matrix, uniform.dimension);
    }
}

}


